/*
 * Copyright 2000-2017 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.intellij.history.integration;

import com.intellij.history.core.LocalHistoryFacade;
import com.intellij.history.core.StoredContent;
import com.intellij.history.core.tree.Entry;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.command.CommandEvent;
import com.intellij.openapi.command.CommandListener;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManagerListener;
import com.intellij.openapi.vfs.VirtualFileVisitor;
import com.intellij.openapi.vfs.newvfs.BulkFileListener;
import com.intellij.openapi.vfs.newvfs.events.*;
import com.intellij.util.containers.DisposableWrapperList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.List;
import java.util.Objects;

class LocalHistoryEventDispatcher implements VirtualFileManagerListener, CommandListener, BulkFileListener {
  private static final Key<Boolean> WAS_VERSIONED_KEY =
    Key.create(LocalHistoryEventDispatcher.class.getSimpleName() + ".WAS_VERSIONED_KEY");

  private final LocalHistoryFacade myVcs;
  private final IdeaGateway myGateway;
  private final DisposableWrapperList<BulkFileListener> myVfsEventListeners = new DisposableWrapperList<>();

  LocalHistoryEventDispatcher(LocalHistoryFacade vcs, IdeaGateway gw) {
    myVcs = vcs;
    myGateway = gw;
  }

  @Override
  public void beforeRefreshStart(boolean asynchronous) {
    beginChangeSet();
  }

  @Override
  public void afterRefreshFinish(boolean asynchronous) {
    endChangeSet(LocalHistoryBundle.message("system.label.external.change"));
  }

  @Override
  public void commandStarted(@NotNull CommandEvent e) {
    beginChangeSet();
  }

  @Override
  public void commandFinished(@NotNull CommandEvent e) {
    endChangeSet(e.getCommandName());
  }

  void startAction() {
    myGateway.registerUnsavedDocuments(myVcs);
    myVcs.forceBeginChangeSet();
  }

  void finishAction(String name) {
    myGateway.registerUnsavedDocuments(myVcs);
    endChangeSet(name);
  }

  private void beginChangeSet() {
    myVcs.beginChangeSet();
  }

  private void endChangeSet(String name) {
    myVcs.endChangeSet(name);
  }

  private void fileCreated(@Nullable VirtualFile file) {
    if (file == null) return;
    beginChangeSet();
    createRecursively(file);
    endChangeSet(null);
  }

  private void createRecursively(VirtualFile f) {
    VfsUtilCore.visitChildrenRecursively(f, new VirtualFileVisitor<Void>() {
      @Override
      public boolean visitFile(@NotNull VirtualFile f) {
        if (isVersioned(f)) {
          myVcs.created(f.getPath(), f.isDirectory());
        }
        return true;
      }

      @Override
      public Iterable<VirtualFile> getChildrenIterable(@NotNull VirtualFile f) {
        // For unversioned files we try to get cached children in hope that they are already generated by content root manager:
        //  cached children may mean that there are versioned sub-folders or sub-files.
        return myGateway.isVersioned(f, true)
               ? IdeaGateway.loadAndIterateChildren(f)
               : IdeaGateway.iterateDBChildren(f);
      }
    });
  }

  private void beforeContentsChange(@NotNull VFileContentChangeEvent e) {
    VirtualFile f = e.getFile();
    if (!myGateway.areContentChangesVersioned(f)) return;

    Pair<StoredContent, Long> content = myGateway.acquireAndUpdateActualContent(f, null);
    if (content != null) {
      myVcs.contentChanged(f.getPath(), content.first, content.second);
    }
  }

  private void handleBeforeEvent(VFileEvent event) {
    if (event instanceof VFileContentChangeEvent) {
      beforeContentsChange((VFileContentChangeEvent)event);
    }
    else if (event instanceof VFilePropertyChangeEvent && ((VFilePropertyChangeEvent)event).isRename() ||
             event instanceof VFileMoveEvent) {
      VirtualFile f = Objects.requireNonNull(event.getFile());
      f.putUserData(WAS_VERSIONED_KEY, myGateway.isVersioned(f));
    }
    else if (event instanceof VFileDeleteEvent) {
      beforeFileDeletion((VFileDeleteEvent)event);
    }
  }

  private void propertyChanged(@NotNull VFilePropertyChangeEvent e) {
    if (e.isRename()) {
      VirtualFile f = e.getFile();

      boolean isVersioned = myGateway.isVersioned(f);
      Boolean wasVersioned = f.getUserData(WAS_VERSIONED_KEY);
      if (wasVersioned == null) return;
      f.putUserData(WAS_VERSIONED_KEY, null);

      if (!wasVersioned && !isVersioned) return;

      String oldName = (String)e.getOldValue();
      myVcs.renamed(f.getPath(), oldName);
    }
    else if (VirtualFile.PROP_WRITABLE.equals(e.getPropertyName())) {
      if (!isVersioned(e.getFile())) return;
      VirtualFile f = e.getFile();
      if (!f.isDirectory()) {
        myVcs.readOnlyStatusChanged(f.getPath(), !(Boolean)e.getOldValue());
      }
    }
  }

  private void fileMoved(@NotNull VFileMoveEvent e) {
    VirtualFile f = e.getFile();

    boolean isVersioned = myGateway.isVersioned(f);
    Boolean wasVersioned = f.getUserData(WAS_VERSIONED_KEY);
    if (wasVersioned == null) return;
    f.putUserData(WAS_VERSIONED_KEY, null);

    if (!wasVersioned && !isVersioned) return;

    myVcs.moved(f.getPath(), e.getOldParent().getPath());
  }

  private void beforeFileDeletion(@NotNull VFileDeleteEvent e) {
    VirtualFile f = e.getFile();
    Entry entry = myGateway.createEntryForDeletion(f);
    if (entry != null) {
      myVcs.deleted(f.getPath(), entry);
    }
  }

  private boolean isVersioned(VirtualFile f) {
    return myGateway.isVersioned(f);
  }

  @Override
  public void before(@NotNull List<? extends VFileEvent> events) {
    myGateway.runWithVfsEventsDispatchContext(events, true, () -> {
      for (VFileEvent event : events) {
        handleBeforeEvent(event);
      }

      for (BulkFileListener listener : myVfsEventListeners) {
        listener.before(events);
      }
    });
  }

  @Override
  public void after(@NotNull List<? extends VFileEvent> events) {
    myGateway.runWithVfsEventsDispatchContext(events, false, () -> {
      for (VFileEvent event : events) {
        handleAfterEvent(event);
      }
      for (BulkFileListener listener : myVfsEventListeners) {
        listener.after(events);
      }
    });
  }

  private void handleAfterEvent(VFileEvent event) {
    if (event instanceof VFileCreateEvent) {
      fileCreated(event.getFile());
    }
    else if (event instanceof VFileCopyEvent) {
      fileCreated(((VFileCopyEvent)event).findCreatedFile());
    }
    else if (event instanceof VFilePropertyChangeEvent) {
      propertyChanged((VFilePropertyChangeEvent)event);
    }
    else if (event instanceof VFileMoveEvent) {
      fileMoved((VFileMoveEvent)event);
    }
  }

  void addVirtualFileListener(BulkFileListener virtualFileListener, Disposable disposable) {
    myVfsEventListeners.add(virtualFileListener, disposable);
  }
}